第14章 智能推荐系统¶
智能推荐系统属于非监督式学习, 是机器学习一个非常重要的应用领域, 它能带来的经济价值往往是直接且非常可观的
14.1 智能推荐系统的基本原理¶
14.1.1 智能推荐系统的应用场景¶
互联网每天都在产生海量信息,用户行为数据呈现爆发式增长用户会有直接和明确的需求,但也可能是在漫无目的地搜寻
智能推荐系统可以通过分析用户的浏览次数、浏览时间、点击率等指标,挖掘出用户感兴趣的内容或商品,然后进行个性化推荐
如果推荐的内容或商品能高效匹配用户的需求,就能优化用户体验,提高用户黏性,创造额外收入
14.1.2 智能推荐系统的基础: 协同过滤算法¶
搭建智能推荐系统的算法有很多, 商业实战中用得较多的是协同过滤算法(collaborative filtering)
协同过滤算法的原理是根据用户群体的产品偏好数据, 发现用户与物品之间的相关性, 并基于这些相关性为用户进行推荐
根据原理的不同, 协同过滤算法分为两类: 基于用户的协同过滤算法和基于物品的协同过滤算法
基于用户的协同过滤算法
基于用户的协同过滤算法的本质是寻找相似的用户: 通过一个用户的相关数据寻找与该用户相似的其他用户, 进而为该用户推荐相似用户关注的产品
比如用户1和用户2都给商品A,B,C打了高分, 那么可以将用户1和用户2划分在同一个用户群体, 此时若用户2还给商品D打了高分, 那么就可以将商品D推荐给用户1
基于物品的协同过滤算法
基于物品的协同过滤算法的本质是寻找相似的物品: 通过一个物品的相关数据取寻找与该物品相似的其他物品, 进而为关注该物品的用户推荐相似的物品
比如图书A和图书B都被用户1,2,3购买过, 那么可以认为图书A和图书B具有较强的相似度, 进而推测喜欢图书A的用户同样也会喜欢图书B, 当用户4购买图书B时, 根据图书A和图书B的相似性, 可以将图书A推荐给用户4
在商业实战中, 大多数应用场景偏向于使用基于物品的协同过滤算法, 主要原因有:
通常情况下, 用户的数量是非常庞大的, 而物品的数量则相对有限, 因此, 计算不同物品的相似度往往比计算不同的用户的相似度容易很多
用户的喜好较为多变, 而物品的属性较明确, 不随时间变化, 过去的用户对物品的评分长期有效, 所以物品的相似度比较固定, 可以预先离线计算好物品的相似度, 把结果存在表中, 需要向用户进行推荐时再从表中调用
14.2 计算相似度的常用方法¶
无论是基于用户还是基于产品的协同过滤算法, 其本质都是寻找数据之间的相似度, 计算相似度的三种常用方法--欧氏距离、余弦值和皮尔逊系数
表中为3个用户对3个物品的评分, 数字代表星级:
因为评分数据都在0-5之间, 量级一致, 所以无须做标准化处理, 如果数据的量级存在较大差异, 应先做标准化处理
14.2.1 欧氏距离¶
欧式距离的计算之前已经说过, 对空间坐标 $A$ 和 $B$ , $A$ 和 $B$ 间的欧氏距离计算公式为:
$$ d(A,B) = ||\overrightarrow{AB}||_2 $$
将前面的用户评分表中的数据带入, 计算物品A和物品B的欧氏距离为:
$$ d(A,B) = \sqrt{(5-4)^2 + (1-2)^2 + (5-2)^2} = \sqrt{11} = 3.32 $$
除了直接比较欧氏距离, 还可以利用欧氏距离衍生出的相似度公式来衡量两者的相似度, 基于欧氏距离的相似度 $sim(A,B)$ 定义为:
$$ sim(A,B) = \frac{1}{1 + d(A,B)} $$
将前面计算出的欧式距离代入, 可以计算出物品A和物品B的相似度为:
$$ sim(A,B) = \frac{1}{1 + 3.32} = 0.23 $$
对前面的用户评分表的计算结果为:
在第7章K近邻算法中也使用了欧式距离来衡量不同样本间的距离, 只不过那里是监督式学习, 这里则是非监督式学习
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df
用户1 | 用户2 | 用户3 | |
---|---|---|---|
物品A | 5 | 1 | 5 |
物品B | 4 | 2 | 2 |
物品C | 4 | 2 | 1 |
import numpy as np
dist = np.linalg.norm(df.iloc[0] - df.iloc[1]) # numpy库可以方便地计算两个向量的欧氏距离
print('%.3f' % dist,)
print('{:.3f}'.format(dist))
print(np.around(dist, decimals=3))
3.317 3.317 3.317
14.2.2 余弦相似度¶
在向量空间中, 向量 $\overrightarrow{a}$ 和向量 $\overrightarrow{b}$ 的夹角的余弦值可以表示为:
$$ cos<\overrightarrow{a},\overrightarrow{b}> = \frac{\overrightarrow{a}\cdot \overrightarrow{b}}{|\overrightarrow{a}||\overrightarrow{b}|} = \frac{\overrightarrow{a}\cdot \overrightarrow{b}}{||\overrightarrow{a}||_2||\overrightarrow{b}||_2} $$
$||x||_p$, $x$ 的 $Lp$ 范数, 当 $p$ 取 $n$ 时, 称为向量 $x$ 的 $Ln-norm$, 计算公式为:
$$ ||x||_p := \sqrt[p]{\sum_{i=0}^{n}|x_i|^p} $$
可以直接用Scikit-Learn库中的cosine_similarity()函数计算各物品之间的余弦相似度, 并整理成DataFrame格式的二维表格:
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df
用户1 | 用户2 | 用户3 | |
---|---|---|---|
物品A | 5 | 1 | 5 |
物品B | 4 | 2 | 2 |
物品C | 4 | 2 | 1 |
from sklearn.metrics.pairwise import cosine_similarity
user_similarity = cosine_similarity(df)
user_similarity = np.around(user_similarity, decimals=3) # 保留三位小数
pd.DataFrame(user_similarity, columns=['物品A', '物品B', '物品C'], index=['物品A', '物品B', '物品C'])
物品A | 物品B | 物品C | |
---|---|---|---|
物品A | 1.000 | 0.915 | 0.825 |
物品B | 0.915 | 1.000 | 0.980 |
物品C | 0.825 | 0.980 | 1.000 |
14.2.3 皮尔逊相关系数¶
(皮尔逊)相关系数 $r$ 时用于描述两个变量间相关强弱程度的统计量, 取值范围为 $[-1,1]$, 为正值代表两个变量存在正相关关系, 为负值代表两个变量存在负相关关系, 其绝对值越大, 说明相关性越强, 计算公式为:
$$ r = corr(X,Y) = \frac{Cov(X,Y)}{\sqrt{D(X)}\sqrt{D(Y)}} = \frac{Cov(X,Y)}{\sqrt{\sigma_X^2}\sqrt{\sigma_Y^2}} = \frac{\sigma_{XY}}{\sigma_X\sigma_Y} $$
from scipy.stats import pearsonr
X = [1, 3, 5, 7, 9]
Y = [9, 8, 6, 4, 2]
corr = pearsonr(X, Y)
print('相关系数r值为 %.3f, 显著性水平P值为 %.3f' % (corr[0],corr[1]))
相关系数r值为 -0.994, 显著性水平P值为 0.001
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df = df.T
df
物品A | 物品B | 物品C | |
---|---|---|---|
用户1 | 5 | 4 | 4 |
用户2 | 1 | 2 | 2 |
用户3 | 5 | 2 | 1 |
# 物品A与其他物品的皮尔逊相关系数
A = df['物品A']
corr_A = df.corrwith(A)
corr_A
物品A 1.000000 物品B 0.500000 物品C 0.188982 dtype: float64
# 皮尔逊系数表,获取各物品相关性
df.corr()
物品A | 物品B | 物品C | |
---|---|---|---|
物品A | 1.000000 | 0.500000 | 0.188982 |
物品B | 0.500000 | 1.000000 | 0.944911 |
物品C | 0.188982 | 0.944911 | 1.000000 |
14.3 案例实战: 电影智能推荐系统¶
14.3.1 案例背景¶
如果视频平台能利用基于物品的智能推荐系统,从用户对观看过的电影给出的评分中有效地挖掘数据,便能根据用户的偏好个性化地推荐更多类似的电影,从而优化用户体验,提高用户黏性,创造额外收入
14.3.2 数据读取与处理¶
import pandas as pd
movies = pd.read_excel('电影.xlsx')
movies.head()
电影编号 | 名称 | 类别 | |
---|---|---|---|
0 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 |
1 | 2 | 勇敢者的游戏(1995) | 冒险|儿童|幻想 |
2 | 3 | 斗气老顽童2(1995) | 喜剧|爱情 |
3 | 4 | 待到梦醒时分(1995) | 喜剧|剧情|爱情 |
4 | 5 | 新娘之父2(1995) | 喜剧 |
score = pd.read_excel('评分.xlsx')
score.head()
用户编号 | 电影编号 | 评分 | |
---|---|---|---|
0 | 1 | 1 | 4.0 |
1 | 1 | 3 | 4.0 |
2 | 1 | 6 | 4.0 |
3 | 1 | 47 | 5.0 |
4 | 1 | 50 | 5.0 |
df = pd.merge(movies, score, on='电影编号')
df.head()
电影编号 | 名称 | 类别 | 用户编号 | 评分 | |
---|---|---|---|---|---|
0 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 | 1 | 4.0 |
1 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 | 5 | 4.0 |
2 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 | 7 | 4.5 |
3 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 | 15 | 2.5 |
4 | 1 | 玩具总动员(1995) | 冒险|动画|儿童|喜剧|幻想 | 17 | 4.5 |
df.to_excel('电影推荐系统.xlsx')
df['评分'].value_counts() # 查看各个评分的出现的次数
评分 4.0 26794 3.0 20017 5.0 13180 3.5 13129 4.5 8544 2.0 7545 2.5 5544 1.0 2808 1.5 1791 0.5 1369 Name: count, dtype: int64
import matplotlib.pyplot as plt
df['评分'].hist(bins=20) # hist()函数绘制直方图,竖轴为各评分出现的次数
<Axes: >
ratings = pd.DataFrame(df.groupby('名称')['评分'].mean())
ratings.sort_values('评分', ascending=False).head()
评分 | |
---|---|
名称 | |
假小子(1997) | 5.0 |
福尔摩斯和华生医生历险记:讹诈之王(1980) | 5.0 |
机器人(2016) | 5.0 |
奥斯卡(1967) | 5.0 |
人类状况III(1961) | 5.0 |
ratings['评分次数'] = df.groupby('名称')['评分'].count()
ratings.sort_values('评分次数', ascending=False).head()
评分 | 评分次数 | |
---|---|---|
名称 | ||
阿甘正传(1994) | 4.164134 | 329 |
肖申克的救赎(1994) | 4.429022 | 317 |
低俗小说(1994) | 4.197068 | 307 |
沉默的羔羊(1991) | 4.161290 | 279 |
黑客帝国(1999) | 4.192446 | 278 |
user_movie = df.pivot_table(index='用户编号', columns='名称', values='评分')
user_movie.tail()
名称 | 007之黄金眼(1995) | 100个女孩(2000) | 100条街道(2016) | 101忠狗续集:伦敦大冒险(2003) | 101忠狗(1961) | 101雷克雅未克(2000) | 102只斑点狗(2000) | 10件或更少(2006) | 10(1979) | 11:14(2003) | ... | 龙珠:神秘冒险(1988) | 龙珠:血红宝石的诅咒(1986) | 龙珠:魔鬼城堡中的睡公主(1987) | 龙种子(1944) | 龙纹身的女孩(2011) | 龙舌兰日出(1988) | 龙虾(2015) | 龙:夜之怒的礼物(2011) | 龙:李小龙的故事(1993) | 龟日记(1985) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
用户编号 | |||||||||||||||||||||
606 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
607 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
608 | 4.0 | NaN | NaN | NaN | NaN | NaN | NaN | 3.5 | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
609 | 4.0 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN |
610 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | ... | NaN | NaN | NaN | NaN | 4.0 | NaN | 4.5 | NaN | NaN | NaN |
5 rows × 9687 columns
user_movie.describe() # 因为数据量较大,这个耗时可能会有1分钟左右
名称 | 007之黄金眼(1995) | 100个女孩(2000) | 100条街道(2016) | 101忠狗续集:伦敦大冒险(2003) | 101忠狗(1961) | 101雷克雅未克(2000) | 102只斑点狗(2000) | 10件或更少(2006) | 10(1979) | 11:14(2003) | ... | 龙珠:神秘冒险(1988) | 龙珠:血红宝石的诅咒(1986) | 龙珠:魔鬼城堡中的睡公主(1987) | 龙种子(1944) | 龙纹身的女孩(2011) | 龙舌兰日出(1988) | 龙虾(2015) | 龙:夜之怒的礼物(2011) | 龙:李小龙的故事(1993) | 龟日记(1985) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 132.000000 | 4.00 | 1.0 | 1.0 | 44.000000 | 1.0 | 9.000000 | 3.000000 | 4.000000 | 4.00 | ... | 1.0 | 1.0 | 2.000000 | 1.0 | 42.000000 | 13.000000 | 7.000000 | 1.0 | 8.00000 | 2.0 |
mean | 3.496212 | 3.25 | 2.5 | 2.5 | 3.431818 | 3.5 | 2.777778 | 2.666667 | 3.375000 | 3.75 | ... | 3.5 | 3.5 | 3.250000 | 3.5 | 3.488095 | 3.038462 | 4.000000 | 5.0 | 2.81250 | 4.0 |
std | 0.859381 | 0.50 | NaN | NaN | 0.751672 | NaN | 0.833333 | 1.040833 | 1.030776 | 0.50 | ... | NaN | NaN | 0.353553 | NaN | 1.327422 | 0.431158 | 0.707107 | NaN | 1.03294 | 0.0 |
min | 0.500000 | 2.50 | 2.5 | 2.5 | 1.500000 | 3.5 | 2.000000 | 1.500000 | 2.000000 | 3.00 | ... | 3.5 | 3.5 | 3.000000 | 3.5 | 0.500000 | 2.000000 | 3.000000 | 5.0 | 0.50000 | 4.0 |
25% | 3.000000 | 3.25 | 2.5 | 2.5 | 3.000000 | 3.5 | 2.000000 | 2.250000 | 3.125000 | 3.75 | ... | 3.5 | 3.5 | 3.125000 | 3.5 | 2.625000 | 3.000000 | 3.500000 | 5.0 | 2.87500 | 4.0 |
50% | 3.500000 | 3.50 | 2.5 | 2.5 | 3.500000 | 3.5 | 2.500000 | 3.000000 | 3.500000 | 4.00 | ... | 3.5 | 3.5 | 3.250000 | 3.5 | 4.000000 | 3.000000 | 4.000000 | 5.0 | 3.00000 | 4.0 |
75% | 4.000000 | 3.50 | 2.5 | 2.5 | 4.000000 | 3.5 | 3.000000 | 3.250000 | 3.750000 | 4.00 | ... | 3.5 | 3.5 | 3.375000 | 3.5 | 4.000000 | 3.000000 | 4.500000 | 5.0 | 3.12500 | 4.0 |
max | 5.000000 | 3.50 | 2.5 | 2.5 | 5.000000 | 3.5 | 4.500000 | 3.500000 | 4.500000 | 4.00 | ... | 3.5 | 3.5 | 3.500000 | 3.5 | 5.000000 | 4.000000 | 5.000000 | 5.0 | 4.00000 | 4.0 |
8 rows × 9687 columns
import warnings
warnings.filterwarnings('ignore')
FG = user_movie['阿甘正传(1994)'] # FG是Forrest Gump(),阿甘英文名称的缩写
pd.DataFrame(FG).head()
阿甘正传(1994) | |
---|---|
用户编号 | |
1 | 4.0 |
2 | NaN |
3 | NaN |
4 | NaN |
5 | NaN |
# axis默认为0,计算user_movie各列与FG的相关系数
corr_FG = user_movie.corrwith(FG)
similarity = pd.DataFrame(corr_FG, columns=['相关系数'])
similarity.head()
相关系数 | |
---|---|
名称 | |
007之黄金眼(1995) | 0.217441 |
100个女孩(2000) | NaN |
100条街道(2016) | NaN |
101忠狗续集:伦敦大冒险(2003) | NaN |
101忠狗(1961) | 0.141023 |
similarity.dropna(inplace=True) # 或写成similarity=similarity.dropna()
similarity.head()
相关系数 | |
---|---|
名称 | |
007之黄金眼(1995) | 0.217441 |
101忠狗(1961) | 0.141023 |
102只斑点狗(2000) | -0.857589 |
10件或更少(2006) | -1.000000 |
11:14(2003) | 0.500000 |
similarity_new = pd.merge(similarity, ratings['评分次数'], left_index=True, right_index=True)
similarity_new.head()
相关系数 | 评分次数 | |
---|---|---|
名称 | ||
007之黄金眼(1995) | 0.217441 | 132 |
101忠狗(1961) | 0.141023 | 44 |
102只斑点狗(2000) | -0.857589 | 9 |
10件或更少(2006) | -1.000000 | 3 |
11:14(2003) | 0.500000 | 4 |
# 第二种合并方式
similarity_new = similarity.join(ratings['评分次数'])
similarity_new.head()
相关系数 | 评分次数 | |
---|---|---|
名称 | ||
007之黄金眼(1995) | 0.217441 | 132 |
101忠狗(1961) | 0.141023 | 44 |
102只斑点狗(2000) | -0.857589 | 9 |
10件或更少(2006) | -1.000000 | 3 |
11:14(2003) | 0.500000 | 4 |
similarity_new[similarity_new['评分次数'] > 20].sort_values(by='相关系数', ascending=False).head() # 选取阈值
相关系数 | 评分次数 | |
---|---|---|
名称 | ||
阿甘正传(1994) | 1.000000 | 329 |
抓狂双宝(1996) | 0.723238 | 31 |
雷神:黑暗世界(2013) | 0.715809 | 21 |
致命吸引力(1987) | 0.701856 | 36 |
X战警:未来的日子(2014) | 0.682284 | 30 |
补充知识点: pandas库的分类函数groupby()函数¶
import pandas as pd
data = pd.DataFrame([['战狼2', '丁一', 6, 8], ['攀登者', '王二', 8, 6], ['攀登者', '张三', 10, 8], ['卧虎藏龙', '李四', 8, 8], ['卧虎藏龙', '赵五', 8, 10]], columns=['电影名称', '影评师', '观前评分', '观后评分'])
data
电影名称 | 影评师 | 观前评分 | 观后评分 | |
---|---|---|---|---|
0 | 战狼2 | 丁一 | 6 | 8 |
1 | 攀登者 | 王二 | 8 | 6 |
2 | 攀登者 | 张三 | 10 | 8 |
3 | 卧虎藏龙 | 李四 | 8 | 8 |
4 | 卧虎藏龙 | 赵五 | 8 | 10 |
means = data.groupby('电影名称')[['观后评分']].mean()
means
观后评分 | |
---|---|
电影名称 | |
卧虎藏龙 | 9.0 |
战狼2 | 8.0 |
攀登者 | 7.0 |
means = data.groupby('电影名称')[['观前评分', '观后评分']].mean()
means
观前评分 | 观后评分 | |
---|---|---|
电影名称 | ||
卧虎藏龙 | 8.0 | 9.0 |
战狼2 | 6.0 | 8.0 |
攀登者 | 9.0 | 7.0 |
means = data.groupby(['电影名称', '影评师'])[['观后评分']].mean()
means
观后评分 | ||
---|---|---|
电影名称 | 影评师 | |
卧虎藏龙 | 李四 | 8.0 |
赵五 | 10.0 | |
战狼2 | 丁一 | 8.0 |
攀登者 | 张三 | 8.0 |
王二 | 6.0 |
count = data.groupby('电影名称')[['观后评分']].count()
count
观后评分 | |
---|---|
电影名称 | |
卧虎藏龙 | 2 |
战狼2 | 1 |
攀登者 | 2 |
count = count.rename(columns={'观后评分':'评分次数'})
count
评分次数 | |
---|---|
电影名称 | |
卧虎藏龙 | 2 |
战狼2 | 1 |
攀登者 | 2 |